const inlineTags = [
  'span', 'b', 's', 'u', 'mark', 'i', 'a', 'img',
  'red', 'px-5'
]

export class YedongHtmlInsertTag {
  input: string;
  len: number;
  cur: number;
  tags: { tag: string, level: number }[]; // tag堆栈，必须封闭
  tagLevel: number;
  isNotLatexEnd: number;

  constructor() {
    this.input = '';
    this.len = 0;
    this.cur = 0;
    this.tags = [];
    this.tagLevel = 0;
    this.isNotLatexEnd = -1;
  }

  noop() {}

  get eof() {
    return this.cur >= this.len;
  }

  isEof(length = 0) {
    return this.cur + length >= this.len;
  }

  peek(offset = 0) {
    return this.input[this.cur + offset];
  }

  consume(length: number): string | null {
    if (length === 0) return null
    const maxEnd = Math.min(this.cur + length, this.len)
    const token = this.input.substring(this.cur, maxEnd);
    this.cur = maxEnd
    return token
    // replaceWhiteSpace ? token.replace(/[\s\n]+/g, ' ') : token
  }

  sub(len: number, start: number) {
    const startIdx = start || this.cur;
    return this.input.substring(startIdx, startIdx + len);
  }

  accept(c: string) {
    const curChar = this.peek();
    if (curChar === c) {
      this.cur++;
    } else {
      console.log(`Unexpected character: '${curChar}' should be '${c}'`);
      return false
    }
    return true;
  }

  consumeSpace() {
    do {
      const curChar = this.peek();
      if (curChar === ' ' || curChar === '\t' || curChar === '\r' || curChar === '\n') {
        this.cur++;
      } else {
        break;
      }
    } while (!this.eof);
  }

  consumeWhile(regex: RegExp) {
    const result: string[] = [];
    do {
      const curChar = this.peek();
      if (regex.test(curChar)) {
        result.push(curChar);
        this.cur++;
      } else {
        break;
      }
    } while (!this.eof);
    return result.join('');
  }

  private isWhiteSpace(c: string) {
    // 首字符为 [a-zA-Z\-_]
    return c == null || c === ' ' || c === '\t';
  }

  private isNameFirst(c: string) {
    // 首字符为 [a-zA-Z\-_]
    return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '-' === c || '_' === c;
  }

  private isNamePart(c: string) {
    // 中间字符为 [a-zA-Z0-9\-_]
    return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '-' === c || '_' === c;
  }

  lookName(offset = 0) {
    let len = offset
    let curChar = this.peek(len);
    if (this.isNameFirst(curChar)) {
      len++
      do {
        curChar = this.peek(len);
        if (this.isNamePart(curChar)) {
          len++;
        } else {
          break;
        }
      } while (!this.isEof(len));
    }

    return {name: this.input.substring(this.cur + offset, this.cur + len), len}
  }

  // pureInput(input: string) {
  //   let puredStr = input.trim();
  //   if (puredStr.startsWith(DOCTYPE_MARK)) {
  //     puredStr = puredStr.substring(DOCTYPE_MARK.length);
  //   }
  //
  //   // 移除 <!-- --> 相关的注释
  //   const delComment = (str: string) => {
  //     const commentStartIdx = str.indexOf(COMMENT_START);
  //     const commentEndIdx = str.indexOf(COMMENT_END);
  //     if (commentStartIdx >= 0 && commentEndIdx >= 0) {
  //       const strBeforeComment = str.substring(0, commentStartIdx);
  //       const strAfterComment = str.substring(commentEndIdx + COMMENT_END.length);
  //       return delComment(`${strBeforeComment}${strAfterComment}`);
  //     }
  //     return str;
  //   };
  //
  //   puredStr = delComment(puredStr);
  //   return puredStr;
  // }

  parse(input = '',newTag = '', options = {}) {
    this.input = input;
    this.len = input.length;
    this.cur = 0;
    this.tags = [];
    this.tagLevel = 0;
    let s = `<${newTag}`
    const tagStack: string[] = []
    // 还要考虑层级
    for (let length = 0; length < this.len; length++) {
      if (this.peek(length) === '<') {
        const {name, len} = this.lookName(length + 1);
        if (name) {
          s += `/${newTag}>`
        } else {
          s += input[length]
        }
      } else if (this.peek(length) === '<' && this.peek(length + 1) === '/') {
        const {name, len} = this.lookName(length + 1);
      }
    }
    return this.parseNodes();
  }

  parseNodes(matchTag = '') {
    const oldTagLevel = this.tagLevel++
    const nodes: any[] = [];
    do {
      let curChar = this.peek()
      if (curChar === '<' || matchTag) {
        if (this.peek(1) === '/') break;
        const element = this.parseElement(matchTag)
        if (element) nodes.push(element);
      } else if (curChar === '$' && this.isNotLatexEnd < 0) {
        const element = this.parseLatex()
        if (element) nodes.push(element);
      } else {
        // 顶级文本，断行后，加 p 标签
        if (this.tagLevel === 1) {
          const children = this.parseParaNodes()
          if (children?.length) {
            nodes.push({tag: 'p', children});
          }
        } else {
          const children = this.parseTextNodes()
          if (children?.length) {
            nodes.push(...children);
          }
        }
        this.isNotLatexEnd = -1
      }
    } while (!this.eof);
    this.tagLevel = oldTagLevel
    return nodes;
  }

  // 多个连续空格当一个空格，多个连续换行当一个 <br/>
  isBlank(c: string) {return c === '\n' || c === '\r' || c === ' ' || c === '\t'}

  isBlanks(c: string | null | undefined) {
    if (!c) return true
    for (const char of c) {
      if (!this.isBlank(char)) return false
    }
    return true
  }

  skipBlanks() {
    let length = 0
    while (this.isBlank(this.peek(length))) {
      length++
    }
    this.cur += length
  }

  /**
   * 1) 顶级文本，加 p 标签
   * 1) 忽略头部和尾部空行
   * 2) 中间空行表示本行段落结束
   * "\n" 替换为 <br/>
   * "    \n" 空行替换为 <p>...</>
   */
  parseParaNodes() {
    const nodes: any[] = [];
    let chars: string[] = []
    let length = 0
    this.skipBlanks()

    const resolveText = () => {
      if (length > 0) {
        this.cur += length
        length = 0
      }
      if (chars.length > 0) {
        // 消耗 length 长度字符
        nodes.push({tag: 'text', props: {text: chars.join('')}})
        chars = []
      }
    }

    do {
      let curChar = this.peek(length);
      // 遇到连续的空白
      let currLength = length
      let newLineCount = 0
      while (this.isBlank(curChar)) {
        if (curChar === '\n') newLineCount++
        length++
        curChar = this.peek(length);
      }
      if (currLength !== length) {
        if (newLineCount > 0) {
          // 如果里面有回车，就插入 1）之前匹配的字符 和 2）换行 <br/>
          resolveText()
          if (newLineCount > 1) {
            // 多个换行，就结束 p 标签
            return nodes
          } else {
            nodes.push({tag: 'br'})
          }
        } else {
          // 仅仅是多个空白字符，用空格代替之
          chars.push(' ')
        }
      }

      // 子标签
      if (curChar === '<') {
        // 如果 < 后面紧跟 tag，代表匹配上 Element
        if (this.isNameFirst(this.peek(length + 1))) {
          const {name} = this.lookName(length + 1)
          if (inlineTags.indexOf(name) !== -1) {
            resolveText()
            nodes.push(this.parseElement())
          } else {
            resolveText()
            return nodes
          }
        } else {
          chars.push(curChar)
          length++;
        }
      } else if (curChar === '$' && (this.cur + length > this.isNotLatexEnd)) {
        // 碰到 Latex 公式
        resolveText()
        const element = this.parseLatex()
        if (element) {
          nodes.push(element)
        }
      } else {
        chars.push(curChar)
        length++;
      }
    } while (!this.isEof(length));

    resolveText()
    this.isNotLatexEnd = -1
    return nodes
  }

  parseTextNodes() {
    const nodes: any[] = [];
    let chars: string[] = []
    let length = 0
    let hasNewLine = true // 当前匹配的是空行
    this.skipBlanks()

    const resolveText = () => {
      if (length > 0) {
        this.cur += length
        length = 0
      }
      if (chars.length > 0) {
        // 消耗 length 长度字符
        nodes.push({tag: 'text', props: {text: chars.join('')}})
        chars = []
      }
    }

    do {
      let curChar = this.peek(length);
      // 遇到连续的空白
      let currLength = length
      while (this.isBlank(curChar)) {
        hasNewLine = curChar === '\n'
        length++
        curChar = this.peek(length);
      }
      if (currLength !== length) {
        if (hasNewLine) {
          // 如果里面有回车，就插入 1）之前匹配的字符 和 2）换行 <br/>
          resolveText()
          nodes.push({tag: 'br'})
        } else {
          // 仅仅是多个空白字符，用空格代替之
          chars.push(' ')
        }
      }

      // 子标签
      if (curChar === '<') {
        // 如果 < 后面紧跟 tag，代表匹配上 Element
        const nextChar = this.peek(length + 1)
        if (this.isNameFirst(nextChar) || nextChar === '/') {
          resolveText()
          return nodes
        } else {
          chars.push(curChar)
          length++;
        }
      } else if (curChar === '$' && (this.cur + length > this.isNotLatexEnd)) {
        // 碰到 Latex 公式
        resolveText()
        return nodes
      } else {
        chars.push(curChar)
        length++;
      }
    } while (!this.isEof(length));

    resolveText()
    return nodes
  }

  parseElement(matchTag = '') {
    this.accept('<');
    const tag = this.parseTag()!;
    this.tags.push({tag, level: this.tagLevel})
    this.consumeSpace();
    // 拿到属性
    const attrs: Record<string, any> = this.parseAttrs()
    const props: Record<string, any> = {inline: inlineTags.indexOf(tag) !== -1}

    if (this.peek() === '/') {
      // 立即封闭标签
      this.accept('/')
      this.accept('>')
      this.tags.pop()
      if (matchTag && tag !== matchTag) {
        return null
      } else {
        return {tag, attrs}
      }
    } else {
      this.accept('>')
      let children: any[] = []
      // 吃掉子节点
      if (tag === 'code') {
        let length = 0
        while (!this.eof) {
          const curChar = this.peek(length);
          if (curChar === '<' && this.peek(length + 1) === '/'
            && this.peek(length + 2) === 'c'
            && this.peek(length + 3) === 'o'
            && this.peek(length + 4) === 'd'
            && this.peek(length + 5) === 'e'
            && this.peek(length + 6) === '>') {
            props.text = this.consume(length)
            break;
          }
          length++
        }
      } else if (tag === 'draw') {
        let length = 0
        while (!this.eof) {
          const curChar = this.peek(length);
          if (curChar === '<' && this.peek(length + 1) === '/'
            && this.peek(length + 2) === 'd'
            && this.peek(length + 3) === 'r'
            && this.peek(length + 4) === 'a'
            && this.peek(length + 5) === 'w'
            && this.peek(length + 6) === '>') {
            props.text = this.consume(length)
            break;
          }
          length++
        }
      } else if (tag === 'hr') {

      } else {
        children = this.parseNodes()
      }

      // 吃掉一个闭标签
      this.accept('<')
      this.accept('/')
      const closeTag = this.parseTag()
      this.consumeSpace()
      this.accept('>')
      const curTag = this.tags.pop()
      if (curTag?.tag !== closeTag) {
        console.log(`未匹配的标签 <${curTag?.tag}>...</${closeTag}>`)
      }
      if (matchTag && tag !== matchTag) {
        return null
      } else {
        return {tag, attrs, props, children}
      }
    }
  }

  parseTag() {
    const {name, len} = this.lookName()
    this.cur += len;
    return name
  }

  parseAttrs() {
    const attrs: Record<string, any> = {};
    // 1) <hr/>
    // 2) <hr name='123'/>
    // 3) <i>@</i>
    // 4) <i name='123'></i>
    // 5) 错误恢复 <hr <p>111</p>
    while (!this.eof && this.peek() !== '/' && this.peek() !== '$' && this.peek() !== '<' && this.peek() !== '>') {
      const name = this.parseTag();
      if (!name) {
        this.accept(this.peek());
        continue;
      }

      if (this.peek() === '=') {
        this.accept('=');
        this.accept('"');
        const value = this.consumeWhile(/[^"]/);
        this.accept('"');
        attrs[name] = value;
      } else {
        attrs[name] = null;
      }
      this.consumeSpace();
    }
    return attrs;
  }

  parseLatex() {
    const startCur = this.cur
    let length = 0
    // 解析恢复，当做普通文本
    // 启发式：1）碰到 $$ 搜索后面是否有 $$ 如果没有，当成内敛元素的两个 $
    // 启发式：2）$$ $$$$ 内容为空，则当成普通文本
    // 启发式：3）直到碰到空行'  \n   \n  '还没碰到 $/$$ 匹配的结束符号
    // 启发式：4）碰到 <tag 或者 </tag 也没关闭
    const recover = (length = 0) => {
      this.isNotLatexEnd = this.cur + length
      this.cur = startCur
    }

    if (!this.accept('$')) return null
    let curChar = this.peek()
    let isBlock = curChar === '$'
    // 向后遍历，没找到 $$ 就认为这是一个空的内联公式 $$
    if (isBlock) {
      let len = 0
      let newLineCount = 0
      curChar = this.peek(++len)
      while (curChar != null && curChar !== '$' && !this.isEof(len)) {
        if (curChar == null || this.eof) {
          isBlock = false
          break
        } else if (this.isBlank(curChar)) {
          if (curChar === '\n') {
            newLineCount++
          }
          if (newLineCount > 1) {
            isBlock = false
            break
          }
        } else {
          newLineCount = 0
          if (curChar === '<') {
            let name = ''
            if (this.peek(1) === '/') {
              name = this.lookName(2).name
            } else {
              name = this.lookName(1).name
            }

            if (name) {
              isBlock = false
              break
            }
          }
        }
        curChar = this.peek(++len)
      }

      if (!isBlock || curChar !== '$' || this.peek(len + 1) !== '$') {
        // curChar(this.cur + len) 为 '$'，恢复到 '$'之前，最后这个 '$' 留作下次匹配
        recover(len - 1)
        return null
      } else {
        this.accept('$')
        curChar = this.peek(length)
      }
    }

    if (curChar == null) {
      recover()
      return null
    }
    let newLineCount = 0
    while (curChar != null && curChar !== '$' && !this.isEof(length)) {
      if (curChar == null || this.eof) {
        recover(length)
        return null
      } else if (this.isBlank(curChar)) {
        if (curChar === '\n') {
          newLineCount++
        }
        if (newLineCount > 1) {
          recover(length)
          return null
        }
      } else {
        // 碰到正常字符，取消(空行) 含多个 '\n' 嫌疑
        newLineCount = 0
        if (curChar === '<') {
          let name = ''
          if (this.peek(length + 1) === '/') {
            name = this.lookName(length + 2).name
          } else {
            name = this.lookName(length + 1).name
          }

          if (name) {
            // 回退，当做普通文本
            recover(length - 1)
            return null
          }
        }
      }
      curChar = this.peek(++length)
    }
    const latex = this.consume(length)
    this.accept('$')
    if (isBlock) this.accept('$')
    if (this.isBlanks(latex)) {
      // curChar(this.cur)为 '$' 的下一个字符，因此要恢复到 '$'
      recover(-1)
      return null
    } else {
      if (isBlock) {
        return {tag: 'block-latex', props: {text: latex}}
      } else {
        return {tag: 'latex', props: {text: latex}}
      }
    }
  }
}

export function insertTag(html: string, options?: any) {
  const parser = new YedongHtmlInsertTag();
  return parser.parse(html, options);
}
